package com.rengwuxian.coursecoroutines._4_flow

import com.rengwuxian.coursecoroutines.common.unstableGitHub
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeoutException
import kotlin.coroutines.EmptyCoroutineContext

/**
 * try/catch 和 Flow 的异常可见性
 * Flow的异常可见性、try/catch、Flow.collect()、emit()
 *
 * 异常管理基本上都是管理的已知异常
 * 未知异常管理基本上都是加一个统一的UncaughtExceptionHandler，用来拦截协程和线程的异常，用来log。
 *
 * 异常是层层往外抛的，emit本身是触发collect的函数型参数，如果上游把emit本身try catch包起来，捕获了异常，没有往外抛，
 * 那么下游的collect外部包着的try catch就不会接收到异常。
 *
 * 上游是生产数据的，不应该把下游消费的代码的异常抓住，导致下游异常不可见，故需要记住一个原则，不能在上游包住emit的异常情况，会闷死这个异常，导致无法到下游。
 *
 * Exception Transparency（异常可见性）：上游的Flow不应该吞掉下游的异常。
 * 只要上游 try/catch 不包住emit就行，或者将异常继续抛出来。
 *
 * 中间有其他操作符，怎么看待异常传递路径？比如map { throw NullPointerException() }
 * map的操作符其实分为两部分，将上游数据进行转换，然后调用下游的emit，发送数据。
 * 故下游collect大括号的异常，不会经历到map转换部分的代码，只会经过那个看不见的emit调用。然后传递给最外上游的emit中。
 * 至于map{}的转换代码里的异常，是会传递给它上游的emit中，然后传递给整个collect的启动过程。
 * 大部分操作符，都是具备这个特点，比如：onEach{}
 * 但是transform操作符是显示调用emit的，它的代码块中是可以捕获到异常的，注意也不要用try/catch包住emit。
 *
 */
fun main() = runBlocking<Unit> {
  val scope = CoroutineScope(EmptyCoroutineContext)
  val flow1 = flow {
    try {
      for (i in 1..5) {
        // 数据库读数据
        // 网络请求
        emit(i)
      }
    } catch (e: Exception) {
      println("Error in flow(): $e")
      throw e
    }
  }.map { throw NullPointerException() }
    .onEach { throw NullPointerException() }
    .transform<Int, Int> {
      val data = it * 2
      emit(data)
      emit(data)
    }
  // Exception Transparency
  scope.launch {
    try {
      flow1.collect {
        val contributors = unstableGitHub.contributors("square", "retrofit")
        println("Contributors: $contributors")
      }
    } catch (e: TimeoutException) {
      println("Network error: $e")
    } catch (e: NullPointerException) {
      println("Null data: $e")
    }
  }
  delay(10000)
}

private fun fun1() {
  fun2()
}

private fun fun2() {
  fun3()
}

private fun fun3() {
  throw NullPointerException("User null")
}
